using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using csammisrun.OscarLib.Utility;

namespace csammisrun.OscarLib
{
    /// <summary>
    /// Contains event information for messages received offline
    /// </summary>
    public class OfflineMessagesReceivedEventArgs : EventArgs
    {
        private readonly ReadOnlyCollection<OfflineIM> receivedMessages;
        private readonly String uin;

        /// <summary>
        /// Initializes a new set of parameters for an offline messages received event
        /// </summary>
        public OfflineMessagesReceivedEventArgs(String uin, Collection<OfflineIM> receivedMessages)
        {
            this.uin = uin;
            this.receivedMessages = new ReadOnlyCollection<OfflineIM>(receivedMessages);
        }

        /// <summary>
        /// The ICQ UIN that received the messages
        /// </summary>
        public String Uin
        {
            get { return uin; }
        }

        /// <summary>
        /// Gets the messages that were received offline
        /// </summary>
        public ReadOnlyCollection<OfflineIM> ReceivedMessages
        {
            get { return receivedMessages; }
        }
    }

    /// <summary>
    /// Contains information concerning a client adding the locally logged in UIN to their list
    /// </summary>
    public class AddedToRemoteListEventArgs : EventArgs
    {
        private string uin;

        /// <summary>
        /// Initializes a new AddedToRemoteListEventArgs
        /// </summary>
        public AddedToRemoteListEventArgs(String uin)
        {
            this.uin = uin;
        }

        /// <summary>
        /// Gets the UIN of the user that added the locally logged in UIN to their list
        /// </summary>
        public String Uin
        {
            get { return uin; }
        }
    }

    /// <summary>
    /// Handles the receipt of offline messages for a UIN
    /// </summary>
    public delegate void OfflineMessagesReceivedEventHandler(object sender,
                                                             OfflineMessagesReceivedEventArgs e);

    /// <summary>
    /// Handles the event of a remote ICQ user adding the locally logged in UIN to their list
    /// </summary>
    public delegate void AddedToRemoteListEventHandler(object sender,
                                                       AddedToRemoteListEventArgs e);

    /// <summary>
    /// An interface implemented by manager objects that handle incoming data packets
    /// </summary>
    public interface ISnacFamilyHandler
    {
        /// <summary>
        /// Process an incoming <see cref="DataPacket"/>
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> received by the server</param>
        void ProcessIncomingPacket(DataPacket dp);
    }

    /// <summary>
    /// Performs the special functions for that special protocol, ICQ
    /// </summary>
    public class IcqManager : ISnacFamilyHandler
    {
        #region Constants

        /// <summary>
        /// ICQ code for deleting offline messages
        /// </summary>
        private const int DELETE_OFFLINE_MESSAGES = 0x003E;

        /// <summary>
        /// ICQ code for the end of the offline message sequence
        /// </summary>
        private const int END_OFFLINE_MESSAGES = 0x0042;

        /// <summary>
        /// ICQ code for received meta-information pertaining to an ICQ account
        /// </summary>
        private const int ICQ_INFORMATION_RECEIVED = 0x0003;

        /// <summary>
        /// ICQ code for a received offline message
        /// </summary>
        private const int OFFLINE_MESSAGE = 0x0041;

        /// <summary>
        /// ICQ code for retrieving offline messages
        /// </summary>
        private const int RETRIEVE_OFFLINE_MESSAGES = 0x003C;

        /// <summary>
        /// SSI code for a contact adding the current UIN to their list
        /// </summary>
        private const int SSI_ADDED_TO_LIST = 0x001C;

        #endregion

        #region Fields

        private readonly Session parent;

        #endregion Fields

        #region Enums

        private enum MetaRequestType : ushort
        {
            META_DATA_REQ = 0x07d0
        }

        private enum MetaRequestSubType : ushort
        {
            UNKNOWN_SEARCH = 0x0fa0
        }

        #endregion Enums

        #region Constructor

        /// <summary>
        /// Initializes a new IcqManager
        /// </summary>
        internal IcqManager(Session parent)
        {
            this.parent = parent;
            this.parent.Dispatcher.RegisterSnacFamilyHandler(this, 0x0015);
        }

        #endregion Constructor

        #region ISnacFamilyHandler Members

        /// <summary>
        /// Process an incoming <see cref="DataPacket"/> from SNAC family 15 (and one from 13)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> received by the server</param>
        public void ProcessIncomingPacket(DataPacket dp)
        {
            Debug.Assert(dp.SNAC.FamilyServiceID == 0x0015 || dp.SNAC.FamilyServiceID == 0x0013);
            switch (dp.SNAC.FamilySubtypeID)
            {
                case ICQ_INFORMATION_RECEIVED:
                    ProcessMetaInformationResponse(dp);
                    break;
                case SSI_ADDED_TO_LIST:
                    ProcessRemoteListAddition(dp);
                    break;
            }
        }

        #endregion

        #region Common Functions

        /// <summary>
        /// Creates a byte stream with a common ICQ prefix
        /// </summary>
        /// <param name="uin">An ICQ UIN</param>
        private static ByteStream BeginIcqByteStream(String uin)
        {
            ByteStream stream = new ByteStream();
            stream.WriteUshort(0x0001);
            stream.WriteUshort(0x000A);
            stream.WriteUshortLE(0x0008);
            stream.WriteUintLE(uint.Parse(uin));
            return stream;
        }

        /// <summary>
        /// Creates a SNAC header with the common ICQ properties
        /// </summary>
        private SNACHeader CreateIcqMetaHeader()
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = (ushort)SNACFamily.ICQExtensionsService;
            sh.FamilySubtypeID = (ushort)ICQExtensionsService.MetaInformationRequest;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();
            return sh;
        }

        private void ProcessMetaInformationResponse(DataPacket dp)
        {
            using (TlvBlock tlvs = new TlvBlock(dp.Data.ReadByteArrayToEnd()))
            {
                if (tlvs.HasTlv(0x0001))
                {
                    ByteStream stream = new ByteStream(tlvs.ReadByteArray(0x0001));
                    ushort cmdLength = stream.ReadUshortLE();
                    String uin = stream.ReadUintLE().ToString();
                    ushort command = stream.ReadUshortLE();
                    ushort requestId = stream.ReadUshortLE();

                    switch (command)
                    {
                        case OFFLINE_MESSAGE:
                            ReadOfflineMessage(uin, stream);
                            break;
                        case END_OFFLINE_MESSAGES:
                            EndOfflineMessageSequence(uin);
                            break;
                    }
                }
            }
        }

        #endregion Common Functions

        #region Offline Message Handling

        /// <summary>
        /// Raised when offline messages have been received for a UIN
        /// </summary>
        public event OfflineMessagesReceivedEventHandler OfflineMessagesReceived;
        
        /// <summary>
        /// A flag indicating whether the manager is currently receiving offline messages
        /// </summary>
        private bool isReceivingOfflineMessages;

        /// <summary>
        /// Gets a value indicating whether the manager is currently reading offline messages
        /// </summary>
        public bool IsReceivingOfflineMessages
        {
            get { return isReceivingOfflineMessages; }
        }
        
        /// <summary>
        /// The list of offline messages collected by this manager
        /// </summary>
        private readonly Dictionary<string, Collection<OfflineIM>> offlineMessages =
            new Dictionary<string, Collection<OfflineIM>>();

        /// <summary>
        /// Retrieve offline messages for the user, which are returned
        /// in the <see cref="OfflineMessagesReceived"/> event
        /// </summary>
        /// <remarks>When messages have been retrieved, <see cref="DeleteOfflineMessages"/>
        /// is automatically called to remove them from the server.</remarks>
        public void RetrieveOfflineMessages()
        {
            if (!ScreennameVerifier.IsValidICQ(parent.ScreenName))
            {
                throw new Exception("Can't retrieve offline messages for non-ICQ accounts");
            }

            if (isReceivingOfflineMessages)
            {
                return;
            }

            isReceivingOfflineMessages = true;

            SNACHeader header = CreateIcqMetaHeader();
            ByteStream stream = BeginIcqByteStream(parent.ScreenName);
            stream.WriteUshortLE(RETRIEVE_OFFLINE_MESSAGES);
            stream.WriteUshortLE((ushort) header.RequestID);
            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, header, stream));
        }

        /// <summary>
        /// Deletes offline messages belonging to the user
        /// </summary>
        /// <remarks>This method also removes any offline messages locally cached by the manager.</remarks>
        public void DeleteOfflineMessages()
        {
            if (!ScreennameVerifier.IsValidICQ(parent.ScreenName))
            {
                throw new Exception("Can't retrieve offline messages for non-ICQ accounts");
            }

            offlineMessages.Remove(parent.ScreenName);

            SNACHeader header = CreateIcqMetaHeader();
            ByteStream stream = BeginIcqByteStream(parent.ScreenName);
            stream.WriteUshortLE(DELETE_OFFLINE_MESSAGES);
            stream.WriteUshortLE((ushort) header.RequestID);
            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, header, stream));
        }

        /// <summary>
        /// Processes a newly received offline message for the specified UIN
        /// </summary>
        private void ReadOfflineMessage(String uin, ByteStream stream)
        {
            String sender = stream.ReadUintLE().ToString();

            // Read in the date information (GMT)
            ushort year = stream.ReadUshortLE();
            byte month = stream.ReadByte();
            byte day = stream.ReadByte();
            byte hourGmt = stream.ReadByte();
            byte minute = stream.ReadByte();
            DateTime received = new DateTime(year, month, day, hourGmt, minute, 0, DateTimeKind.Utc);

            // Read in message type information
            byte messageType = stream.ReadByte();
            byte messageFlags = stream.ReadByte();
            Encoding messageEncoding = GetOfflineMessageEncoding(messageType);

            OfflineIM im = new OfflineIM(sender);
            im.ReceivedOn = received;
            im.Message = Encoding.Unicode.GetString(
                Encoding.Convert(messageEncoding, Encoding.Unicode, stream.ReadByteArray(stream.ReadUshortLE() - 1)));
            im.IsAutoResponse = (messageFlags == 0x03) || (messageType == 0xE8);

            // Store it for delivery to the client
            AcceptIcbmOIM(im);
        }

        /// <summary>
        /// Accepts an offline message received by the <see cref="MessageManager"/>
        /// </summary>
        internal void AcceptIcbmOIM(OfflineIM message)
        {
            if (!offlineMessages.ContainsKey(parent.ScreenName))
            {
                offlineMessages.Add(parent.ScreenName, new Collection<OfflineIM>());
            }

            offlineMessages[parent.ScreenName].Add(message);
        }

        /// <summary>
        /// Infers the message encoding from the message type flag
        /// </summary>
        /// <param name="messageType">The message type flag received in the offline message notification</param>
        /// <returns>Either <see cref="Encoding.ASCII"/> or <see cref="Encoding.Unicode"/></returns>
        /// <remarks>According to Shutko, there are several message types that are "0xFE formatted."</remarks>
        private static Encoding GetOfflineMessageEncoding(byte messageType)
        {
            switch (messageType)
            {
                case 0x04:
                case 0x06:
                case 0x07:
                case 0x09:
                case 0x0C:
                case 0x0D:
                case 0x0E:
                    return Encoding.Unicode;
                default:
                    return Encoding.ASCII;
            }
        }

        /// <summary>
        /// Processes a message from the server that offline message retrival is finished
        /// </summary>
        /// <remarks>This method sends a message to the server to delete any already retrieved
        /// offline messages.</remarks>
        private void EndOfflineMessageSequence(String uin)
        {
            Collection<OfflineIM> oims = new Collection<OfflineIM>();
            if (offlineMessages.ContainsKey(uin))
            {
                oims = offlineMessages[uin];
                DeleteOfflineMessages();
            }

            isReceivingOfflineMessages = false;

            // Raise the offline messages received event
            if (OfflineMessagesReceived != null)
            {
                OfflineMessagesReceived(this, new OfflineMessagesReceivedEventArgs(uin, oims));
            }
        }
        #endregion Offline Message Handling

        #region Added to remote list handling

        /// <summary>
        /// Raised when a remote ICQ user adds the locally logged in UIN to their list
        /// </summary>
        public event AddedToRemoteListEventHandler AddedToRemoteList;

        private void ProcessRemoteListAddition(DataPacket dp)
        {
            String uin = dp.Data.ReadString(dp.Data.ReadByte(), Encoding.ASCII);
            if (AddedToRemoteList != null)
            {
                AddedToRemoteList(this, new AddedToRemoteListEventArgs(uin));
            }
        }

        #endregion Added to remote list handling
        
        #region Search Handling

        /// <summary>
        /// Detailed search for an icq members
        /// </summary>
        /// <param name="nickname">the nickname of the members, null if this should be no search criteria</param>
        /// <param name="firstname">the firstname of the members, null if this should be no search criteria</param>
        /// <param name="lastname">the lastname of the members,null if this should be no search criteria</param>
        /// <param name="fromAge">the minimum age of the members, value=0 if this should be no search criteria</param>
        /// <param name="toAge">the maximum age of the members, value=0 if this should be no search criteria</param>
        /// <param name="gender">the gender of the members</param>
        /// <param name="language">the spoken language of the members</param>
        /// <param name="country">the country of the members</param>
        /// <param name="online">Determines if the members have to be online to appear in the result list</param>
        public void SendSearchRequest(string nickname, string firstname, string lastname, ushort fromAge, ushort toAge, 
            SearchGender gender, SearchLanguage language,  SearchCountry country,SearchOnline online)
        {
            SNACHeader sh = this.CreateIcqMetaHeader();


            Tlv metaDataTlv = new Tlv();
            metaDataTlv.TypeNumber = 0x0001;

            ByteStream helpStream = new ByteStream();
            helpStream.WriteUintLE(uint.Parse(this.parent.ScreenName));

            helpStream.WriteUshortLE((ushort)MetaRequestType.META_DATA_REQ);

            ushort sequenceNumber = 0x0000;
            helpStream.WriteUshortLE(sequenceNumber);

            helpStream.WriteUshortLE((ushort)MetaRequestSubType.UNKNOWN_SEARCH);

            ByteStream subHelpStream = new ByteStream();

            #region Unknown
            byte[] unknownBytes = new byte[] { 0x05, 0xb9, 0x00, 0x02, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x04, 0xe4, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01 };
            subHelpStream.WriteByteArray(unknownBytes);
            #endregion Unknown

            #region Search criteria
            TlvBlock block = new TlvBlock();

            ByteStream ageStream = new ByteStream();
            ageStream.WriteUshort(toAge);
            ageStream.WriteUshort(fromAge);

            if (!String.IsNullOrEmpty(nickname))
            {
                block.WriteString((int)SearchTlvType.NickName, nickname, this.parent.Encoding);
            }
            if (gender != SearchGender.None)
            {
                block.WriteByte((int)SearchTlvType.Gender, (byte)gender);
            }
            if (online != SearchOnline.Offline)
            {
                block.WriteUshort((int)SearchTlvType.Online, (ushort)online);
            }
            if (fromAge > 0 && toAge > 0)
            {
                block.WriteByteArray((int)SearchTlvType.Age, ageStream.GetBytes());
            }
            if (country != SearchCountry.Undefined)
            {
                block.WriteUint((int)SearchTlvType.Country, (uint)country);
            }
            if (language != SearchLanguage.Undefined)
            {
                block.WriteUshort((int)SearchTlvType.Language, (ushort)language);
            }
            if (!String.IsNullOrEmpty(lastname))
            {
                block.WriteString((int)SearchTlvType.LastName, lastname, this.parent.Encoding);
            } 
            if (!String.IsNullOrEmpty(firstname))
            {
                block.WriteString((int)SearchTlvType.FirstName, firstname, this.parent.Encoding);
            }

            subHelpStream.WriteUshort((ushort)block.GetByteCount());
            subHelpStream.WriteTlvBlock(block);
            #endregion Search criteria

            helpStream.WriteUshortLE((ushort)subHelpStream.GetByteCount());
            helpStream.WriteByteArray(subHelpStream.GetBytes());

            ByteStream metaDataStream = new ByteStream();
            metaDataStream.WriteUshortLE((ushort)helpStream.GetByteCount());
            metaDataStream.WriteByteArray(helpStream.GetBytes());
            metaDataTlv.DataSize = (ushort)metaDataStream.GetByteCount();

            TlvBlock metaDataTlvBlock = new TlvBlock();
            metaDataTlvBlock.WriteByteArray(0x0001, metaDataStream.GetBytes());

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(this.parent, sh, new ByteStream(metaDataTlvBlock.GetBytes())));

        }


        private enum SearchTlvType : ushort
        {
            /// <summary>
            /// the nickname
            /// </summary>
            NickName = 0x0078,

            /// <summary>
            /// the firstname
            /// </summary>
            FirstName = 0x0064,

            /// <summary>
            /// the lastname
            /// </summary>
            LastName = 0x006e,

            /// <summary>
            /// Length = 4
            /// First value (ushort) = upper age limit
            /// Second value (ushort) = lower age limit
            /// </summary>
            Age = 0x0154,

            /// <summary>
            /// <see cref="SearchGender"/>
            /// </summary>
            Gender = 0x0082,

            /// <summary>
            /// <see cref="SearchLanguage"/>
            /// </summary>
            Language = 0x00fa,

            /// <summary>
            /// <see cref="SearchCountry"/>
            /// </summary>
            Country = 0x00be,

            /// <summary>
            /// <see cref="SearchOnline"/>
            /// </summary>
            Online = 0x0136,
        }

        /// <summary>
        /// The language creteria for an icq members search
        /// </summary>
        public enum SearchLanguage : ushort
        {
            Undefined = 0x0000,

            English = 0x000c,

            German = 0x0013,

            French = 0x0011
        }

        /// <summary>
        /// The gender creteria for an icq members search
        /// </summary>
        public enum SearchGender : byte
        {
            /// <summary>
            /// Default, if the Gender Tlv is not present
            /// </summary>
            None = 0x00,

            /// <summary>
            /// Female
            /// </summary>
            Female = 0x01,

            /// <summary>
            /// Male
            /// </summary>
            Male = 0x02
        }

        /// <summary>
        /// The country creteria for an icq members search
        /// </summary>
        public enum SearchCountry : uint
        {
            /// <summary>
            /// No country defined
            /// </summary>
            Undefined = 0x00000000,

            /// <summary>
            /// United States (USA)
            /// </summary>
            UnitedStates = 0x00000001,

            /// <summary>
            /// France
            /// </summary>
            France = 0x00000021,

            /// <summary>
            /// Germany
            /// </summary>
            Germany = 0x00000031

        }

        /// <summary>
        /// The online creteria for an icq members search
        /// </summary>
        public enum SearchOnline : ushort
        {
            /// <summary>
            /// Default, if the Online Tlv is not present 
            /// </summary>
            Offline = 0x0000,

            /// <summary>
            /// Online
            /// </summary>
            Online = 0x0001,
        }

        #endregion Search Handling

    }
}